/*******************************************************************************
* Copyright (c) 2015 Jeff Martin.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public
* License v3.0 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* Contributors:
* Jeff Martin - initial API and implementation
******************************************************************************/
package cuchaz.enigma.gui;
import java.awt.Font;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import javax.swing.JOptionPane;
import javax.swing.JTextArea;
import tk.wurst_client.enigma.regexlist.RegexListEntry;
import tk.wurst_client.enigma.regexlist.RegexListReader;
import com.google.common.collect.Lists;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import com.strobel.decompiler.languages.java.ast.CompilationUnit;
import cuchaz.enigma.Deobfuscator;
import cuchaz.enigma.Deobfuscator.ProgressListener;
import cuchaz.enigma.analysis.*;
import cuchaz.enigma.gui.ProgressDialog.ProgressRunnable;
import cuchaz.enigma.mapping.*;
public class GuiController
{
private Deobfuscator m_deobfuscator;
private Gui m_gui;
private SourceIndex m_index;
private ClassEntry m_currentObfClass;
private boolean m_isDirty;
private Deque<EntryReference<Entry, Entry>> m_referenceStack;
private static final AtomicInteger counter = new AtomicInteger();
/**
* <p>
* <img src="https://www.debuggex.com/i/l2IVvEmQvol11QAf.png">
* <p>
* <a href="https://www.debuggex.com/r/l2IVvEmQvol11QAf">View on
* Debuggex</a>
* <p>
* Fixes generic types by converting things like
* <p>
* <code>List<String> v1 = (List<String>)Lists.newArrayList();</code>
* <p>
* to
* <p>
* <code>List<String> v1 = Lists.<String>newArrayList();</code>
*/
private Pattern generics =
Pattern
.compile("\\((?:\\w+)\\<((?:\\w{2,}(?:\\<(?:\\w+|\\?(?: extends \\w+)?|, )+\\>|(?:\\[\\])+|)|, |\\.)+)\\>\\)(\\w+)\\.(\\w+)");
/**
* <p>
* <img src="https://www.debuggex.com/i/ojlBdjjZhnfH5FwU.png">
* <p>
* <a href="https://www.debuggex.com/r/ojlBdjjZhnfH5FwU">View on
* Debuggex</a>
* <p>
* Fixes more generic types by converting things like
* <p>
* <code>this.field2833 = (Map<String, T>)Maps.newHashMap();</code>
* <p>
* to
* <p>
* <code>this.field2833 = Maps.newHashMap();</code>
* <p>
* <var>T</var> is not a class, but a generic type that Procyon failed to
* decompile.
*/
private Pattern generics2 =
Pattern
.compile("\\((?:\\w+)\\<(?:[A-Z\\?]|[A-Z\\?], \\w+|\\w+, [A-Z\\?]|[A-Z\\?], [A-Z\\?])>\\)((?:\\w|\\.|\\(\\))+)");
/**
* <p>
* <img src="https://www.debuggex.com/i/kVZtC3FTNidWR86H.png">
* <p>
* <a href="https://www.debuggex.com/r/kVZtC3FTNidWR86H">View on
* Debuggex</a>
* <p>
* Fixes more generic types by converting things like
* <p>
* <code>new Qt<Object>(6.0f, 1.0, 1.2);</code>
* <p>
* to
* <p>
* <code>new Qt(6.0f, 1.0, 1.2);</code>
*/
private Pattern generics3 = Pattern
.compile("(new (?:\\w|\\.)+)\\<Object\\>(\\(.+\\))");
private ArrayList<RegexListEntry> regexList =
new ArrayList<RegexListEntry>();
public GuiController(Gui gui)
{
m_gui = gui;
m_deobfuscator = null;
m_index = null;
m_currentObfClass = null;
m_isDirty = false;
m_referenceStack = Queues.newArrayDeque();
}
public boolean isDirty()
{
return m_isDirty;
}
public void openJar(final JarFile jar) throws IOException
{
m_gui.onStartOpenJar();
m_deobfuscator = new Deobfuscator(jar);
m_gui.onFinishOpenJar(m_deobfuscator.getJarName());
refreshClasses();
}
public void closeJar()
{
m_deobfuscator = null;
m_gui.onCloseJar();
}
public void openMappings(File file) throws IOException,
MappingParseException
{
FileReader in = new FileReader(file);
m_deobfuscator.setMappings(new MappingsReader().read(in));
in.close();
m_isDirty = false;
m_gui.setMappingsFile(file);
refreshClasses();
refreshCurrentClass();
}
public void saveMappings(File file) throws IOException
{
FileWriter out = new FileWriter(file);
new MappingsWriter().write(out, m_deobfuscator.getMappings());
out.close();
m_isDirty = false;
}
public void closeMappings()
{
m_deobfuscator.setMappings(null);
m_gui.setMappingsFile(null);
refreshClasses();
refreshCurrentClass();
}
public void openRegexList(File file)
{
try
{
regexList = new RegexListReader().read(file);
}catch(FileNotFoundException e)
{
e.printStackTrace();
JOptionPane.showMessageDialog(m_gui.getFrame(),
e.getLocalizedMessage(), "File not found",
JOptionPane.ERROR_MESSAGE);
}catch(IOException e)
{
e.printStackTrace();
JTextArea message = new JTextArea(e.getLocalizedMessage());
message.setEditable(false);
message.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
JOptionPane.showMessageDialog(m_gui.getFrame(), message,
"File could not be read", JOptionPane.ERROR_MESSAGE);
}
}
public void closeRegexList()
{
regexList.clear();
}
public void exportSource(final File dirOut)
{
ProgressDialog.runInThread(m_gui.getFrame(), new ProgressRunnable()
{
@Override
public void run(ProgressListener progress) throws Exception
{
m_deobfuscator.writeSources(dirOut, progress);
}
});
}
public void wurstExportSource(final File dirOut)
{
m_currentObfClass = null;
ProgressDialog.runInThread(m_gui.getFrame(), new ProgressRunnable()
{
@Override
public void run(ProgressListener progress) throws Exception
{
progress.init(m_deobfuscator.getJarIndex().getObfClassEntries()
.size(), "Preparing classes...");
int i = 0;
for(ClassEntry entry : m_deobfuscator.getJarIndex()
.getObfClassEntries())
{
if(!entry.isInnerClass())
{
progress.onProgress(i++, entry.getName());
continue;
}
// That string will likely never occur anywhere
// TODO: Allow the user to specify a custom string
String name = "WurstWurstWurstAllesWirdAusWurstGemacht";
try
{
name +=
m_deobfuscator
.getMappings()
.getClassByObf(entry.getOuterClassName())
.getDeobfInnerClassName(
entry.getInnermostClassName());
}catch(Exception e1)
{
name += entry.getInnermostClassName();
}
try
{
EntryReference<Entry, Entry> obfReference =
m_deobfuscator
.obfuscateReference(new EntryReference<Entry, Entry>(
entry, entry.getName()));
m_deobfuscator.rename(obfReference.getNameableEntry(),
name);
}catch(IllegalNameException e)
{
e.printStackTrace();
}
progress.onProgress(i++, entry.getName());
}
// get the classes to decompile
Set<ClassEntry> classEntries = Sets.newHashSet();
for(ClassEntry obfClassEntry : m_deobfuscator.getJarIndex()
.getObfClassEntries())
{
// skip inner classes
if(obfClassEntry.isInnerClass())
continue;
classEntries.add(obfClassEntry);
}
if(progress != null)
progress.init(classEntries.size(), "Decompiling classes...");
// DEOBFUSCATE ALL THE THINGS!! @_@
i = 0;
for(ClassEntry obfClassEntry : classEntries)
{
ClassEntry deobfClassEntry =
m_deobfuscator.deobfuscateEntry(new ClassEntry(
obfClassEntry));
if(progress != null)
progress.onProgress(i++, deobfClassEntry.toString());
try
{
// get the source
String source =
m_deobfuscator.getSource(m_deobfuscator
.getSourceTree(obfClassEntry.getName()));
// fix inner class references
source =
source
.replace(
"$WurstWurstWurstAllesWirdAusWurstGemacht",
".");
source =
source.replace(
"WurstWurstWurstAllesWirdAusWurstGemacht", "");
// fix generic types
source =
generics.matcher(source).replaceAll(
"$2\\.\\<$1\\>$3");
source = generics2.matcher(source).replaceAll("$1");
source = generics3.matcher(source).replaceAll("$1$2");
// apply custom regexes
try
{
for(RegexListEntry entry : regexList)
if(entry.isTarget(obfClassEntry.getName()))
source = entry.replaceAll(source);
}catch(IllegalArgumentException e)
{
e.printStackTrace();
JOptionPane.showMessageDialog(m_gui.getFrame(),
"Regex list contains invalid replacement(s).\n"
+ "Export aborted.", "Invalid regex list",
JOptionPane.ERROR_MESSAGE);
return;
}
// write the file
File file =
new File(dirOut, deobfClassEntry.getName().replace(
'.', '/')
+ ".java");
file.getParentFile().mkdirs();
try(FileWriter out = new FileWriter(file))
{
out.write(source);
}
}catch(Throwable t)
{
throw new Error("Unable to deobfuscate class "
+ deobfClassEntry.toString() + " ("
+ obfClassEntry.toString() + ")", t);
}
}
// FIXME: Changing the names back
// progress.init(m_deobfuscator.getJarIndex().getObfClassEntries()
// .size(), "Renaming inner classes back...");
// i = 0;
// for(ClassEntry entry : m_deobfuscator.getJarIndex()
// .getObfClassEntries())
// {
// if(!entry.isInnerClass())
// {
// progress.onProgress(i++, entry.getName());
// continue;
// }
// String name = entry.getInnermostClassName();
// if(name
// .startsWith("WurstWurstWurstAllesWirdAusWurstGemacht"))
// name =
// name.substring("WurstWurstWurstAllesWirdAusWurstGemacht"
// .length());
// try
// {
// EntryReference<Entry, Entry> obfReference =
// m_deobfuscator
// .obfuscateReference(new EntryReference<Entry, Entry>(
// entry, entry.getName()));
// m_deobfuscator.rename(obfReference.getNameableEntry(),
// name);
// }catch(IllegalNameException e)
// {
// e.printStackTrace();
// }
// progress.onProgress(i++, name);
// }
}
});
}
public void exportJar(final File fileOut)
{
ProgressDialog.runInThread(m_gui.getFrame(), new ProgressRunnable()
{
@Override
public void run(ProgressListener progress)
{
m_deobfuscator.writeJar(fileOut, progress);
}
});
}
public Token getToken(int pos)
{
if(m_index == null)
return null;
return m_index.getReferenceToken(pos);
}
public EntryReference<Entry, Entry> getDeobfReference(Token token)
{
if(m_index == null)
return null;
return m_index.getDeobfReference(token);
}
public ReadableToken getReadableToken(Token token)
{
if(m_index == null)
return null;
return new ReadableToken(m_index.getLineNumber(token.start),
m_index.getColumnNumber(token.start),
m_index.getColumnNumber(token.end));
}
public boolean entryHasDeobfuscatedName(Entry deobfEntry)
{
return m_deobfuscator.hasDeobfuscatedName(m_deobfuscator
.obfuscateEntry(deobfEntry));
}
public boolean entryIsInJar(Entry deobfEntry)
{
return m_deobfuscator.isObfuscatedIdentifier(m_deobfuscator
.obfuscateEntry(deobfEntry));
}
public boolean referenceIsRenameable(
EntryReference<Entry, Entry> deobfReference)
{
return m_deobfuscator.isRenameable(m_deobfuscator
.obfuscateReference(deobfReference));
}
public ClassInheritanceTreeNode getClassInheritance(
ClassEntry deobfClassEntry)
{
ClassEntry obfClassEntry =
m_deobfuscator.obfuscateEntry(deobfClassEntry);
ClassInheritanceTreeNode rootNode =
m_deobfuscator
.getJarIndex()
.getClassInheritance(
m_deobfuscator
.getTranslator(TranslationDirection.Deobfuscating),
obfClassEntry);
return ClassInheritanceTreeNode.findNode(rootNode, obfClassEntry);
}
public ClassImplementationsTreeNode getClassImplementations(
ClassEntry deobfClassEntry)
{
ClassEntry obfClassEntry =
m_deobfuscator.obfuscateEntry(deobfClassEntry);
return m_deobfuscator.getJarIndex().getClassImplementations(
m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating),
obfClassEntry);
}
public MethodInheritanceTreeNode getMethodInheritance(
MethodEntry deobfMethodEntry)
{
MethodEntry obfMethodEntry =
m_deobfuscator.obfuscateEntry(deobfMethodEntry);
MethodInheritanceTreeNode rootNode =
m_deobfuscator
.getJarIndex()
.getMethodInheritance(
m_deobfuscator
.getTranslator(TranslationDirection.Deobfuscating),
obfMethodEntry);
return MethodInheritanceTreeNode.findNode(rootNode, obfMethodEntry);
}
public MethodImplementationsTreeNode getMethodImplementations(
MethodEntry deobfMethodEntry)
{
MethodEntry obfMethodEntry =
m_deobfuscator.obfuscateEntry(deobfMethodEntry);
List<MethodImplementationsTreeNode> rootNodes =
m_deobfuscator
.getJarIndex()
.getMethodImplementations(
m_deobfuscator
.getTranslator(TranslationDirection.Deobfuscating),
obfMethodEntry);
if(rootNodes.isEmpty())
return null;
if(rootNodes.size() > 1)
System.err.println("WARNING: Method " + deobfMethodEntry
+ " implements multiple interfaces. Only showing first one.");
return MethodImplementationsTreeNode.findNode(rootNodes.get(0),
obfMethodEntry);
}
public FieldReferenceTreeNode getFieldReferences(FieldEntry deobfFieldEntry)
{
FieldEntry obfFieldEntry =
m_deobfuscator.obfuscateEntry(deobfFieldEntry);
FieldReferenceTreeNode rootNode =
new FieldReferenceTreeNode(
m_deobfuscator
.getTranslator(TranslationDirection.Deobfuscating),
obfFieldEntry);
rootNode.load(m_deobfuscator.getJarIndex(), true);
return rootNode;
}
public BehaviorReferenceTreeNode getMethodReferences(
BehaviorEntry deobfBehaviorEntry)
{
BehaviorEntry obfBehaviorEntry =
m_deobfuscator.obfuscateEntry(deobfBehaviorEntry);
BehaviorReferenceTreeNode rootNode =
new BehaviorReferenceTreeNode(
m_deobfuscator
.getTranslator(TranslationDirection.Deobfuscating),
obfBehaviorEntry);
rootNode.load(m_deobfuscator.getJarIndex(), true);
return rootNode;
}
public void rename(EntryReference<Entry, Entry> deobfReference,
String newName)
{
EntryReference<Entry, Entry> obfReference =
m_deobfuscator.obfuscateReference(deobfReference);
m_deobfuscator.rename(obfReference.getNameableEntry(), newName);
m_isDirty = true;
refreshClasses();
refreshCurrentClass(obfReference);
}
public void fixNames()
{
if(m_deobfuscator == null)
{
JOptionPane.showMessageDialog(m_gui.getFrame(),
"Cannot fix names because no classes are present.",
"No classes", JOptionPane.ERROR_MESSAGE);
return;
}
m_currentObfClass = null;
ProgressDialog.runInThread(m_gui.getFrame(), new ProgressRunnable()
{
@Override
public void run(ProgressListener progress) throws Exception
{
progress.init(m_deobfuscator.getJarIndex().getObfFieldEntries()
.size(), "Fixing field names...");
counter.set(0);
int i = 0;
for(FieldEntry entry : m_deobfuscator.getJarIndex()
.getObfFieldEntries())
{
String name = entry.getName();
name = "field" + counter.incrementAndGet();
if(!name.equals(entry.getName()))
try
{
rename(new EntryReference<Entry, Entry>(entry,
entry.getName()), name);
}catch(IllegalNameException e)
{}
progress.onProgress(i++, name);
}
progress.init(m_deobfuscator.getJarIndex().getObfClassEntries()
.size(), "Fixing class names...");
counter.set(0);
i = 0;
for(ClassEntry entry : m_deobfuscator.getJarIndex()
.getObfClassEntries())
{
String name = entry.getName();
if(entry.isInnerClass())
name = "Class" + counter.incrementAndGet();
else
name =
name.substring(0, name.lastIndexOf("/") + 1)
+ Character.toUpperCase(name.charAt(name
.lastIndexOf("/") + 1))
+ (entry.getSimpleName().length() == 1
? counter.incrementAndGet() : name
.substring(name.lastIndexOf("/") + 2));
if(!name.equals(entry.getName()))
try
{
rename(new EntryReference<Entry, Entry>(entry,
entry.getName()), name);
}catch(IllegalNameException e)
{}
progress.onProgress(i++, name);
}
}
});
}
public void removeMapping(EntryReference<Entry, Entry> deobfReference)
{
EntryReference<Entry, Entry> obfReference =
m_deobfuscator.obfuscateReference(deobfReference);
m_deobfuscator.removeMapping(obfReference.getNameableEntry());
m_isDirty = true;
refreshClasses();
refreshCurrentClass(obfReference);
}
public void markAsDeobfuscated(EntryReference<Entry, Entry> deobfReference)
{
EntryReference<Entry, Entry> obfReference =
m_deobfuscator.obfuscateReference(deobfReference);
m_deobfuscator.markAsDeobfuscated(obfReference.getNameableEntry());
m_isDirty = true;
refreshClasses();
refreshCurrentClass(obfReference);
}
public void openDeclaration(Entry deobfEntry)
{
if(deobfEntry == null)
throw new IllegalArgumentException("Entry cannot be null!");
openReference(new EntryReference<Entry, Entry>(deobfEntry,
deobfEntry.getName()));
}
public void openReference(EntryReference<Entry, Entry> deobfReference)
{
if(deobfReference == null)
throw new IllegalArgumentException("Reference cannot be null!");
// get the reference target class
EntryReference<Entry, Entry> obfReference =
m_deobfuscator.obfuscateReference(deobfReference);
ClassEntry obfClassEntry =
obfReference.getLocationClassEntry().getOutermostClassEntry();
if(!m_deobfuscator.isObfuscatedIdentifier(obfClassEntry))
throw new IllegalArgumentException("Obfuscated class "
+ obfClassEntry + " was not found in the jar!");
if(m_currentObfClass == null
|| !m_currentObfClass.equals(obfClassEntry))
{
// deobfuscate the class, then navigate to the reference
m_currentObfClass = obfClassEntry;
deobfuscate(m_currentObfClass, obfReference);
}else
showReference(obfReference);
}
private void showReference(EntryReference<Entry, Entry> obfReference)
{
EntryReference<Entry, Entry> deobfReference =
m_deobfuscator.deobfuscateReference(obfReference);
Collection<Token> tokens = m_index.getReferenceTokens(deobfReference);
if(tokens.isEmpty())
// DEBUG
System.err.println(String.format(
"WARNING: no tokens found for %s in %s", deobfReference,
m_currentObfClass));
else
m_gui.showTokens(tokens);
}
public void savePreviousReference(
EntryReference<Entry, Entry> deobfReference)
{
m_referenceStack
.push(m_deobfuscator.obfuscateReference(deobfReference));
}
public void openPreviousReference()
{
if(hasPreviousLocation())
openReference(m_deobfuscator.deobfuscateReference(m_referenceStack
.pop()));
}
public boolean hasPreviousLocation()
{
return !m_referenceStack.isEmpty();
}
private void refreshClasses()
{
List<ClassEntry> obfClasses = Lists.newArrayList();
List<ClassEntry> deobfClasses = Lists.newArrayList();
m_deobfuscator.getSeparatedClasses(obfClasses, deobfClasses);
m_gui.setObfClasses(obfClasses);
m_gui.setDeobfClasses(deobfClasses);
}
private void refreshCurrentClass()
{
refreshCurrentClass(null);
}
private void refreshCurrentClass(EntryReference<Entry, Entry> obfReference)
{
if(m_currentObfClass != null)
deobfuscate(m_currentObfClass, obfReference);
}
private void deobfuscate(final ClassEntry classEntry,
final EntryReference<Entry, Entry> obfReference)
{
m_gui.setSource("(deobfuscating...)");
// run the deobfuscator in a separate thread so we don't block the GUI
// event queue
new Thread()
{
@Override
public void run()
{
// decompile,deobfuscate the bytecode
CompilationUnit sourceTree =
m_deobfuscator.getSourceTree(classEntry.getClassName());
if(sourceTree == null)
{
// decompilation of this class is not supported
m_gui.setSource("Unable to find class: " + classEntry);
return;
}
String source = m_deobfuscator.getSource(sourceTree);
m_index = m_deobfuscator.getSourceIndex(sourceTree, source);
m_gui.setSource(m_index.getSource());
if(obfReference != null)
showReference(obfReference);
// set the highlighted tokens
List<Token> obfuscatedTokens = Lists.newArrayList();
List<Token> deobfuscatedTokens = Lists.newArrayList();
List<Token> otherTokens = Lists.newArrayList();
for(Token token : m_index.referenceTokens())
{
EntryReference<Entry, Entry> reference =
m_index.getDeobfReference(token);
if(referenceIsRenameable(reference))
{
if(entryHasDeobfuscatedName(reference
.getNameableEntry()))
deobfuscatedTokens.add(token);
else
obfuscatedTokens.add(token);
}else
otherTokens.add(token);
}
m_gui.setHighlightedTokens(obfuscatedTokens,
deobfuscatedTokens, otherTokens);
}
}.start();
}
}